{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Problem: Write a GAN\n",
    "\n",
    "### Problem Statement\n",
    "Implement a **Generative Adversarial Network (GAN)** by completing the required sections. The GAN consists of a **Generator** that creates fake data and a **Discriminator** that classifies data as real or fake.\n",
    "\n",
    "### Requirements\n",
    "1. **Define the Generator Class**:\n",
    "   - **Purpose**: Generate fake data that mimics the real data distribution.\n",
    "   - **Layers**:\n",
    "     - Start with a fully connected layer to map the latent space (random noise) to a higher-dimensional space.\n",
    "     - Use activation functions like `ReLU` to introduce non-linearity.\n",
    "     - Add additional layers to process the data and refine its structure.\n",
    "     - The final layer should output data in the target shape. Use an activation function like `Tanh` for scaling.\n",
    "   - **Forward Pass**: Implement the forward method to pass the input through the defined layers.\n",
    "\n",
    "2. **Define the Discriminator Class**:\n",
    "   - **Purpose**: Classify data as real or fake.\n",
    "   - **Layers**:\n",
    "     - Use fully connected layers to process the input and extract features.\n",
    "     - Apply activation functions like `LeakyReLU` to prevent dead neurons and stabilize training.\n",
    "     - The final layer should output a single probability (real or fake) using a `Sigmoid` activation.\n",
    "   - **Forward Pass**: Implement the forward method to process the input through the layers.\n",
    "\n",
    "3. **Train the GAN**:\n",
    "   - Alternate between training the Generator and Discriminator.\n",
    "   - Use binary cross-entropy loss for both models.\n",
    "   - Monitor the loss and generated samples during training."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define the Generator\n",
    "#TODO: Implement the Generator class\n",
    "class Generator(nn.Module):\n",
    "    def __init__(self, input_dim, output_dim):\n",
    "        super(Generator, self).__init__()\n",
    "        # Add layers for the generator\n",
    "\n",
    "    def forward(self, x):\n",
    "        # Define the forward pass for the generator\n",
    "        ...\n",
    "\n",
    "# Define the Discriminator\n",
    "#TODO: Implement the Discriminator class\n",
    "class Discriminator(nn.Module):\n",
    "    def __init__(self, input_dim):\n",
    "        super(Discriminator, self).__init__()\n",
    "        # Add layers for the discriminator\n",
    "\n",
    "    def forward(self, x):\n",
    "        # Define the forward pass for the discriminator\n",
    "        ..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generate synthetic data for training\n",
    "torch.manual_seed(42)\n",
    "real_data = torch.rand(100, 1) * 2 - 1  # 100 samples in the range [-1, 1]\n",
    "\n",
    "# Initialize models, loss, and optimizers\n",
    "latent_dim = 10\n",
    "data_dim = 1\n",
    "G = Generator(latent_dim, data_dim)\n",
    "D = Discriminator(data_dim)\n",
    "\n",
    "criterion = nn.BCELoss()\n",
    "optimizer_G = optim.Adam(G.parameters(), lr=0.001)\n",
    "optimizer_D = optim.Adam(D.parameters(), lr=0.001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch [100/1000] - Loss D: 0.7453, Loss G: 1.3754\n",
      "Epoch [200/1000] - Loss D: 0.9934, Loss G: 0.9999\n",
      "Epoch [300/1000] - Loss D: 1.5028, Loss G: 0.7752\n",
      "Epoch [400/1000] - Loss D: 1.5499, Loss G: 0.6336\n",
      "Epoch [500/1000] - Loss D: 1.1611, Loss G: 0.9280\n",
      "Epoch [600/1000] - Loss D: 1.4005, Loss G: 0.6811\n",
      "Epoch [700/1000] - Loss D: 1.3849, Loss G: 0.7017\n",
      "Epoch [800/1000] - Loss D: 1.3813, Loss G: 0.6896\n",
      "Epoch [900/1000] - Loss D: 1.3936, Loss G: 0.6837\n",
      "Epoch [1000/1000] - Loss D: 1.3767, Loss G: 0.7024\n"
     ]
    }
   ],
   "source": [
    "# Training loop\n",
    "epochs = 1000\n",
    "for epoch in range(epochs):\n",
    "    # Train Discriminator\n",
    "    latent_samples = torch.randn(real_data.size(0), latent_dim)\n",
    "    fake_data = G(latent_samples).detach()\n",
    "    real_labels = torch.ones(real_data.size(0), 1)\n",
    "    fake_labels = torch.zeros(real_data.size(0), 1)\n",
    "\n",
    "    optimizer_D.zero_grad()\n",
    "    real_loss = criterion(D(real_data), real_labels)\n",
    "    fake_loss = criterion(D(fake_data), fake_labels)\n",
    "    loss_D = real_loss + fake_loss\n",
    "    loss_D.backward()\n",
    "    optimizer_D.step()\n",
    "\n",
    "    # Train Generator\n",
    "    latent_samples = torch.randn(real_data.size(0), latent_dim)\n",
    "    fake_data = G(latent_samples)\n",
    "    optimizer_G.zero_grad()\n",
    "    loss_G = criterion(D(fake_data), real_labels)\n",
    "    loss_G.backward()\n",
    "    optimizer_G.step()\n",
    "\n",
    "    # Log progress every 100 epochs\n",
    "    if (epoch + 1) % 100 == 0:\n",
    "        print(f\"Epoch [{epoch + 1}/{epochs}] - Loss D: {loss_D.item():.4f}, Loss G: {loss_G.item():.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Generated data: [[-0.9691686034202576], [0.5036526322364807], [0.4356441795825958], [0.4666149914264679], [-0.6747403144836426]]\n"
     ]
    }
   ],
   "source": [
    "# Generate new samples with the trained Generator\n",
    "latent_samples = torch.randn(5, latent_dim)\n",
    "with torch.no_grad():\n",
    "    generated_data = G(latent_samples)\n",
    "    print(f\"Generated data: {generated_data.tolist()}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
